前一天建立的 etcd 已經能用多數決保存一致的協調狀態。今天要加入 Patroni,由它持續檢查 PostgreSQL、執行資料庫角色變更,並把 PostgreSQL 原生串流複寫與 etcd 的領導者鎖定連接起來。
本文先從 PostgreSQL 程序、資料路徑與控制路徑開始,再依序說明 Patroni 設定、首次建立、複本加入、控制迴圈與 REST API。最後才建立三節點高可用性(High Availability,HA)叢集,確認只有一台主節點(Primary)、兩台複本(Replica),而且 PostgreSQL 的生命週期已交由 Patroni 管理。
本文閱讀方式
- 完整理解技術與底層原理:依序閱讀全文。
- 掌握完成部署所需的基本概念:優先閱讀標有「⭐」的章節。
- 快速了解當天的部署內容:閱讀文末的 Lab/實作章節。
- 跟著系列建立完整系統:依照 GitHub 詳細部署文件 的天數順序操作。
看到這裡,你可能會疑惑:前幾天不是已經完成多個 PostgreSQL 執行個體的串流複寫,為什麼今天還要建立叢集?先釐清叢集在這裡的意思。PostgreSQL 官方所稱的資料庫叢集(Database Cluster),是由單一 PostgreSQL 伺服器執行個體管理的一組資料庫與資料目錄。本系列所說的 PostgreSQL HA 叢集,則是多個 PostgreSQL 執行個體共同維持一個可用資料庫服務的架構。
前面建立的手動串流複寫已經能把 Primary 的 WAL 傳到 Replica,解決資料副本同步。角色判斷、故障後由哪台接手、舊 Primary 如何安全回歸,以及各節點的生命週期需要人工處理。Patroni 接著補上這層控制能力,透過 etcd 協調單一 Primary,並由各節點上的控制迴圈持續管理角色與 PostgreSQL 程序。

圖(一)Patroni 是本系列用來協調 PostgreSQL 角色與生命週期的高可用性框架。
Patroni 是以 Python 開發的開源 PostgreSQL 高可用性框架。它在每台資料庫節點上執行,檢查本機 PostgreSQL 狀態,透過 DCS 協調角色,並在必要時管理 PostgreSQL 的啟動、停止、提升、降級與複本初始化。資料由 PostgreSQL 保存並透過原生串流複寫傳送。
DCS 是 Patroni 對協調狀態儲存層的統稱,提供所有 Patroni 節點共同讀寫一致叢集狀態的位置。本系列由 etcd 擔任 DCS,保存成員狀態、共用設定、初始化標記與具有期限的領導者 Key。Patroni 讀取這些資料後,判斷哪個節點可以維持 Primary、哪些節點應保持 Replica。簡單來說,DCS 是 Patroni 使用的協調儲存層,etcd 是實際提供這項能力的服務。
前面兩天完成的元件分工可以整理如下:
| 元件 | 主要責任 | 不負責的工作 |
|---|---|---|
| PostgreSQL | 處理 SQL、保存資料並透過 WAL 執行串流複寫 | 跨節點安全選出唯一可寫角色 |
| etcd(本系列的 DCS) | 以 Raft、多數決與租約保存少量一致狀態 | 分析 WAL、啟停 PostgreSQL 或傳送資料 |
| Patroni | 檢查 PostgreSQL 與 DCS 狀態,管理程序、角色及複寫設定 | 儲存資料表、轉送 SQL 或取代 WAL 複寫 |
應用程式連線直接進入 PostgreSQL 的 TCP 5432,WAL 也由 PostgreSQL 傳給 Replica。Patroni 與 etcd 位於控制路徑,SQL 則沿著資料路徑傳送:

圖(二)SQL 與 WAL 位於資料路徑,Patroni、etcd 與 REST API 則組成資料庫角色的控制路徑。
這個分層有兩個重要結果。第一,Patroni 或 etcd 短暫無法回應時,既有 SQL 連線可能繼續使用。若目前 Primary 無法持續確認自己的領導者鎖定,Patroni 便會採取保守動作,避免兩台資料庫同時接受寫入。第二,Patroni 選出資料庫角色後,還需要另一層固定入口把新的應用程式連線送往正確節點。今天先完成角色控制,固定連線入口留待後續建立。
未導入 Patroni 時,Debian 可以透過 postgresql.service、版本化的 systemd 服務單元(Unit)或 pg_ctlcluster 管理 PostgreSQL。導入 Patroni 後,同一個資料目錄的啟停、角色與復原設定必須改由 Patroni 統一控制。
如果作業系統服務和 Patroni 同時管理同一個 PostgreSQL 執行個體,可能出現兩邊反覆啟停程序、重複占用 TCP 5432,或舊節點在錯誤時機重新接受寫入。正確關係是由 systemd 維持 Patroni 程序,再由 Patroni 啟動並監督 PostgreSQL:

圖(三)作業系統管理 Patroni,Patroni 再成為 PostgreSQL 執行個體唯一的生命週期管理者。
操作入口必須符合責任邊界:查詢資料庫狀態使用 SQL。檢查叢集、重新載入設定與維護角色則使用 patronictl 或受保護的 REST API。特殊修復若需要暫時手動控制 PostgreSQL,應先讓 Patroni 進入維護模式並依維運手冊(Runbook)操作,確保同一時間只有一套控制來源生效。
Patroni 設定由本機設定、動態設定,以及只在第一次建立叢集時使用的 Bootstrap 區塊共同組成,三台主機的 YAML 也會包含各自的節點名稱、位址與憑證路徑。
| 設定類型 | 保存位置 | 適合保存的內容 | 變更方式 |
|---|---|---|---|
| 本機設定(Local Configuration) | 每台節點的 Patroni YAML 或環境變數 | name、監聽位址、連線位址、資料目錄、憑證路徑與本機標籤 |
修改該節點設定後重新載入 Patroni |
| 動態設定(Dynamic Configuration) | DCS | ttl、loop_wait、retry_timeout、候選延遲門檻及需要整個叢集一致的 PostgreSQL 參數 |
使用 patronictl edit-config 或 REST API 修改 |
| 首次建立設定(Bootstrap Configuration) | 最初寫在 YAML,建立時轉成 DCS 初始設定 | initdb、初始 pg_hba、初始動態設定與建立後動作 |
只在叢集第一次建立時使用 |
以下骨架只保留辨識設定層級所需的欄位。scope、namespace、etcd 端點與 Bootstrap 內容由三台共用。name、REST 位址、PostgreSQL 位址與資料目錄則依節點調整。可直接部署的 TLS、驗證規則與帳密設定留在實作文件。
scope: iron-pg
namespace: /service/
name: pg01
restapi:
listen: 10.77.30.11:8008
connect_address: 10.77.30.11:8008
etcd3:
hosts: 10.77.30.11:2379,10.77.30.12:2379,10.77.30.13:2379
protocol: https
cacert: /etc/patroni/etcd-tls/ca.crt
cert: /etc/patroni/etcd-tls/node.crt
key: /etc/patroni/etcd-tls/node.key
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576
postgresql:
use_pg_rewind: true
use_slots: true
postgresql:
listen: 10.77.30.11:5432
connect_address: 10.77.30.11:5432
data_dir: /var/lib/postgresql/18/patroni
bin_dir: /usr/lib/postgresql/18/bin
一般 Patroni 設定中,環境變數可以覆蓋本機設定,本機設定又能覆蓋多數動態值。但部分維持 PostgreSQL 叢集一致性所需的參數只能由動態設定管理,另有少數安全值會由 Patroni 直接強制設定。因此修改後要以 Patroni 顯示的有效設定與 pending restart 狀態判斷實際結果。
scope、namespace 與 name 組成叢集身分三個欄位看起來相似,實際上分別回答叢集、DCS 路徑與節點身分:
| 欄位 | 本次值 | 意義 |
|---|---|---|
scope |
iron-pg |
三台 Patroni 共同加入的 PostgreSQL 叢集名稱 |
namespace |
/service/ |
Patroni 在 DCS 使用的路徑前綴 |
name |
pg01、pg02、pg03 |
每個 Patroni 成員的唯一名稱 |
相同 namespace 與 scope 讓三台節點讀寫同一組 DCS 狀態,不同 name 則讓每台能發布自己的 REST 位址、PostgreSQL 位址、角色與複寫進度。若不同環境誤用相同路徑,可能互相看見不屬於自己的成員。若三台 scope 不同,則會被視為三套互不相關的叢集。
以本次 namespace: /service/ 與 scope: iron-pg 為例,Patroni 會在 /service/iron-pg/ 下交換少量控制資料。實際 Key 會隨 Patroni 版本與啟用功能略有增減,核心內容如下:
| etcd 路徑 | 保存內容 | 更新時機 |
|---|---|---|
/service/iron-pg/initialize |
PostgreSQL 叢集的初始化標記與系統識別 | 某台節點取得初始化權時以原子操作建立 |
/service/iron-pg/config |
ttl、loop_wait、複寫參數等動態設定 |
首次建立及管理者修改共用設定時 |
/service/iron-pg/leader |
目前持有資料庫領導者鎖定的 Patroni 成員名稱 | 取得鎖定後建立,Primary 持續更新有效期限 |
/service/iron-pg/members/pg01 等 |
各節點的 PostgreSQL/REST 位址、角色、狀態、時間軸與標籤等成員資料 | 每台 Patroni 在控制迴圈中更新 |
/service/iron-pg/status |
最近的 Primary WAL 位置與永久複寫槽狀態 | 目前領導者在狀態改變時更新 |
/service/iron-pg/history |
PostgreSQL 時間軸與角色切換歷史 | 發生角色切換後更新 |
執行計畫性切換或故障切換時可能出現 /failover Key。啟用同步複寫時,/sync Key 則保存目前 Primary 與同步複本狀態。etcd 保存的內容限於協調狀態。SQL 查詢、資料表內容、完整 WAL、資料庫密碼與 TLS 私鑰保存在各自的資料庫或檔案路徑。這些控制資料讓每台 Patroni 對叢集身分、目前角色與候選條件取得一致看法。
第一次啟動時,DCS 尚未存在這個 scope 的初始化狀態。三台 Patroni 可以接近同時啟動並各自嘗試建立 initialize Key,但 etcd 的原子操作只會讓其中一台成功。取得初始化權的節點依照 Bootstrap 設定執行 initdb 或自訂建立方法,準備 PostgreSQL、初始角色、驗證規則與動態設定,再成為第一個 Primary。此處的初始化權只負責協調一次性的叢集建立。叢集運作期間則由可續期的領導者 Key 決定哪一台 PostgreSQL 維持 Primary。
其他節點看見 DCS 已完成初始化後,會加入既有叢集。它們會尋找可以複製的來源,預設使用 PostgreSQL 的 pg_basebackup 建立一致的資料目錄,再接續 WAL 成為 Replica:

圖(四)三台 Patroni 都會嘗試建立 initialize Key,etcd 只讓其中一台取得初始化權並執行 Bootstrap。其餘兩台讀取已建立的叢集狀態後,主動連向 Primary 執行 pg_basebackup,從 Primary 取得基礎備份並持續接收 WAL,最後形成一台 Primary 與兩台 Replica。
bootstrap.dcs 與 bootstrap.pg_hba 只在首次建立時生效。叢集建立後,共用設定改由 patronictl edit-config 或 REST API 管理。本機名稱、位址與憑證路徑等節點差異則繼續留在各自主機的 YAML。
本次先保存舊資料目錄與邏輯備份(Dump),再由 Patroni 建立新的乾淨叢集。等一台 Primary 與兩台 Replica 成形後,只對目前 Primary 還原 Dump,資料再經由 PostgreSQL 串流複寫到兩台 Replica。pg_basebackup 在這裡負責建立新複本的共同起點。獨立備份與災難復原需要另外規劃。
每台資料庫節點上的 Patroni 都會執行高可用性控制迴圈(HA Loop)。每輪會讀取 DCS、查詢本機 PostgreSQL、發布成員資訊,然後根據領導者鎖定、資料庫角色、複寫位置與節點標籤決定下一個動作。

圖(五)每台 Patroni 反覆比對 DCS 與本機 PostgreSQL 狀態,再維持或修正目前角色。
etcd 的 Raft 領導者(Leader)、Patroni 領導者鎖定持有者與 PostgreSQL Primary 是三個不同概念。Raft Leader 負責協調 etcd 日誌。各 Patroni 程序則競爭 DCS 中的資料庫領導者 Key。成功持有並持續更新該 Key 的 Patroni,才有資格讓自己的 PostgreSQL 維持 Primary。PostgreSQL 狀態比較與 promote 動作由 Patroni 負責,etcd 專注保存一致的協調狀態。
| 參數 | 本次值 | 作用 |
|---|---|---|
loop_wait |
10 秒 | 一輪正常控制迴圈結束後的等待時間 |
retry_timeout |
10 秒 | Patroni 對 DCS 或 PostgreSQL 操作重試使用的時間界線 |
ttl |
30 秒 | 領導者鎖定的有效期限 |
Patroni 要求這些參數符合:
loop_wait + 2 × retry_timeout ≤ ttl
本次設定:10 + 2 × 10 = 30 秒
ttl 代表領導鎖的租約期限,復原時間目標(Recovery Time Objective,RTO)則要從明確的故障起點量測到服務恢復。實際中斷還會包含故障發生在控制迴圈中的位置、DCS 重試、候選節點判斷、PostgreSQL 提升,以及用戶端重新建立連線所需的時間。今天先確認控制迴圈與參數關係。角色切換與服務恢復時間要在具備明確起點、終點與用戶端探測後量測。
maximum_lag_on_failover 則限制落後超過指定 WAL 位元組數的 Replica 參與自動接手。本次設為 1048576,也就是 1 MiB。這項數值只定義候選資格門檻。實際資料遺失範圍需透過故障測試量測,並與事先定義的復原點目標(Recovery Point Objective,RPO)比較。
每台 Patroni 預設透過 REST API 對外提供本機狀態。它一方面供 Patroni 成員與 patronictl 查詢或管理叢集,另一方面也能讓代理服務與監控系統用 HTTP 狀態碼判斷節點是否符合特定角色。
| REST 端點 | HTTP 200 代表的狀態 | 適合用途 |
|---|---|---|
/primary 或 /read-write |
PostgreSQL 正常,而且本機是持有領導者鎖定的 Primary | 尋找可寫節點 |
/replica |
PostgreSQL 正常、本機是 Replica,且未設定 noloadbalance |
尋找可讀複本 |
/replica?lag=1MB |
除了符合 Replica 條件,複寫落後也在門檻內 | 排除過度落後的唯讀節點 |
/health |
本機 PostgreSQL 正在執行 | 單純檢查程序健康,不判斷角色 |
/patroni |
回傳本機 Patroni 與 PostgreSQL 的詳細狀態 | 維運與監控判讀 |
同一個節點的 /health 與 /primary 可能分別回傳 200 與 503:前者只說 PostgreSQL 正常,後者還要求它是持有鎖定的 Primary。這也是健康、角色與能否接受特定流量必須分開檢查的原因。
REST 健康檢查只回報 Patroni 已經決定的角色,代理服務再依結果選擇新連線要送往哪個後端節點。具有切換、重新啟動、重新初始化與修改設定能力的 REST 方法則屬於管理介面,TCP 8008 僅開放給 Patroni 節點、受控管理、監控及必要的代理來源。
以下命令涵蓋設定驗證、角色、動態設定、資料庫實際狀態、REST 端點與服務紀錄,適合在日常檢查時依序使用:
# 驗證本機 Patroni YAML
sudo -u postgres patroni --validate-config /etc/patroni/config.yml
# 查看三台成員與目前角色
sudo -u postgres patronictl -c /etc/patroni/config.yml list
# 查看 DCS 內的共用動態設定
sudo -u postgres patronictl -c /etc/patroni/config.yml show-config iron-pg
# 由 PostgreSQL 自身確認本機是否處於復原狀態
sudo -u postgres psql -d postgres -Atc 'SELECT pg_is_in_recovery();'
# 在目前 Primary 查看兩台 Replica 的串流狀態
sudo -u postgres psql -d postgres -P pager=off \
-c "SELECT application_name,client_addr,state,sync_state FROM pg_stat_replication ORDER BY application_name;"
# 比較三台節點的 Primary 端點,正常情況只有一台回傳 HTTP 200
for host in 10.77.30.11 10.77.30.12 10.77.30.13; do
printf '%s ' "$host"
curl -sS -o /dev/null -w 'HTTP %{http_code}\n' \
"http://${host}:8008/primary"
done
# 查看 Patroni 最近的服務紀錄
sudo journalctl -u patroni -n 100 --no-pager
三個 Patroni 程序正在執行只能確認程序存活。PostgreSQL HA 叢集成立還要交叉確認以下狀態:
| 要確認的關係 | 觀察方式 | 正常結果 |
|---|---|---|
| Patroni 成員與角色 | patronictl list |
三個成員、單一 Leader 與兩個 Replica |
| PostgreSQL 實際角色 | pg_is_in_recovery() |
Primary 為 false,Replica 為 true |
| PostgreSQL 複寫連線 | pg_stat_replication |
Primary 看見兩條 streaming 連線 |
| REST 角色端點 | /primary、/replica |
只有符合該角色的節點回傳 200 |
| 共用動態設定 | patronictl show-config |
三台共用相同 DCS 設定 |
| 程序管理關係 | systemd 與程序資訊 | Patroni 啟用,套件預設 PostgreSQL 服務不再自動管理本次執行個體 |
今天的完成條件是建立並讀懂這個穩定狀態。叢集基線確認正確後,計畫性切換、非計畫性故障、時間軸改變、pg_rewind 與舊 Primary 回歸再以獨立測試驗證,讓初始設定與故障處置的結果可以分開判讀。
本次先保存原有的手動串流複寫資料,再建立新的 Patroni 資料目錄。實作會先單獨啟動 pg01,讓它取得初始化權並建立叢集,再依序啟動 pg02、pg03,以 Replica 身分加入。最後只在目前 Primary 還原測試資料,確認資料能透過 PostgreSQL 原生串流複寫抵達兩台 Replica。
完整的安裝、設定檔、TLS 憑證路徑、systemd 服務單元、啟動順序與資料還原命令放在文件庫:
GitHub 實作文件:Day 23|PostgreSQL HA 叢集實作(上):使用 Patroni 完成自動選主與狀態管理
scope、namespace、DCS 端點與 Bootstrap 內容相同,name、listen 與 connect_address 各自唯一。pg_hba 規則解析結果與 pg_rewind 前置條件。/primary、/replica 與 /health 的 HTTP 結果,確認 REST API 表達的角色與 PostgreSQL 實際狀態一致。三台設定使用相同的 scope: iron-pg、namespace: /service/ 與三個 etcd 端點,但 name、REST API 位址與 PostgreSQL 位址分別對應 pg01、pg02、pg03。三份設定都必須先通過 patroni --validate-config,並由 postgres 帳號讀取 etcd 用戶端私鑰。

圖(六)pg01 的 Patroni 設定通過驗證,節點名稱、REST API、PostgreSQL、資料目錄與三個 etcd 端點均符合本次規劃,postgres 也能讀取 etcd 用戶端私鑰。
本次建立新的 /var/lib/postgresql/18/patroni 資料目錄,原本的資料目錄則保存為 main.day22。套件預設的 postgresql.service 停用後,由 patroni.service 維持開機啟動。執行中的 PostgreSQL 程序應由 Patroni 啟動並指向新的資料目錄。

圖(七)pg01 已停用套件預設的 PostgreSQL 服務,Patroni 則保持啟用及執行,實際資料目錄為 /var/lib/postgresql/18/patroni。
patronictl list 應同時列出 pg01、pg02、pg03,而且只有一個成員顯示 Leader,其餘兩個顯示 Replica。再從 PostgreSQL 查詢 pg_is_in_recovery(),可確認 Patroni 顯示的角色和資料庫本身一致。兩項結果共同驗證 Patroni 已實際管理本次 PostgreSQL 叢集的角色。
本次由 pg01 完成首次建立,但 Leader 是由 Patroni 動態維持的角色,後續重新選主時可能改由 pg02 或 pg03 接手。本組驗證畫面拍攝時 pg03 是 Leader。判讀時應確認叢集維持一台 Leader 與兩台 Replica,擔任 Primary 的節點則可隨重新選主改變。

圖(八)patronictl list 顯示 pg03 是 Leader,pg01、pg02 是 Replica。pg01 的 pg_is_in_recovery() 為 true,與 Patroni 角色一致。

圖(九)pg02 的 pg_is_in_recovery() 為 true,確認第二台複本的 PostgreSQL 實際角色。

圖(十)pg03 的 pg_is_in_recovery() 為 false,與 patronictl list 顯示的 Leader 角色一致。
叢集建立後,patronictl show-config iron-pg 應顯示 ttl: 30、loop_wait: 10、retry_timeout: 10、maximum_lag_on_failover: 1048576、use_slots: true 與 use_pg_rewind: true。HBA 檔案還要包含 rewind_user 使用的 hostssl 規則。pg_hba_file_rules 用來確認目前檔案內容與解析結果。實際 pg_rewind 可用性會在舊 Primary 回歸測試中驗證。

圖(十一)DCS 動態設定保留本次使用的 loop_wait、retry_timeout、ttl、候選延遲門檻、複寫槽與 pg_rewind 設定。

圖(十二)pg_hba_file_rules 顯示 rewind_user 的 hostssl 規則限制來源為資料庫網段,驗證方式為 SCRAM-SHA-256,且規則沒有解析錯誤。
對三台節點分別查詢 /primary 與 /replica。目前 Primary 的 /primary 應回傳 200,兩台 Replica 的 /replica 應回傳 200。不符合角色的端點則回傳非 200。再查詢 /health,可以看出程序健康和資料庫角色是兩種不同條件。

圖(十三)pg03 的 /primary 回傳 HTTP 200,pg01、pg02 的 /replica 回傳 HTTP 200,三台 /health 皆回傳 HTTP 200。
既有邏輯備份只還原到目前 Primary。Primary 的 pg_stat_replication 應看到另外兩台 Replica 的兩條 streaming 連線,兩台 Replica 的 pg_is_in_recovery() 應為 true。本次再以 pg01 查詢 replication_demo,確認資料筆數、ID 範圍與最新內容和 Primary 一致。這些結果驗證資料由 PostgreSQL 原生 WAL 複寫傳送,Patroni 專注管理角色與生命週期。

圖(十四)目前 Primary pg03 看見 pg01、pg02 兩條 streaming 連線,本機 pg_is_in_recovery() 回傳 false,測試資料共有 38 筆。

圖(十五)Replica pg01 處於復原狀態,資料筆數與 ID 範圍均和 Primary 相同,也能讀取最新五筆資料。
| 要回答的問題 | 證據 | 判讀方式 |
|---|---|---|
| 三台是否屬於同一 Patroni 叢集 | scope、namespace、DCS 端點與 patroni --validate-config |
共用叢集識別與 DCS,節點名稱及位址各自唯一 |
| PostgreSQL 是否交由 Patroni 管理 | systemd 狀態、資料目錄與 PostgreSQL 程序 | Patroni 啟用,套件預設 PostgreSQL 服務停用,本次執行個體使用 Patroni 資料目錄 |
| 是否只有一台可寫節點 | patronictl list 與 pg_is_in_recovery() |
一台 Leader/false,兩台 Replica/true |
| Bootstrap 設定是否已寫入 DCS | patronictl show-config |
顯示本次計時、複寫槽與 pg_rewind 設定 |
pg_rewind 驗證規則是否能正確解析 |
pg_hba_file_rules |
rewind_user 的 hostssl 規則存在且沒有解析錯誤 |
| REST API 是否正確表達角色 | 三台 /primary、/replica 與 /health |
HTTP 200 出現在符合相應角色與健康條件的節點 |
| PostgreSQL 與 Patroni 連接埠是否只接受規劃來源 | nftables 規則與正反連線測試 | pg 節點可以互連,ca01 無法直接連線 5432/8008 |
| 資料是否由 PostgreSQL 複寫 | pg_stat_replication、復原狀態與 pg01 資料查詢 |
Primary 看見兩條串流連線,pg01 的資料與 Primary 一致 |
pending restart。需要重啟時一次處理一個 Replica,確認叢集健康後再繼續。patronictl、PostgreSQL 查詢與 REST API 交叉確認單一 Primary、兩台 Replica 及串流複寫。下一篇是 Day 24|PostgreSQL HA 叢集實作(下):計畫性切換、故障切換與舊 Primary 安全回歸。我們會在今天建立的 Patroni 三節點叢集上執行計畫性切換與故障切換,觀察領導鎖、資料庫角色與 PostgreSQL 時間軸如何改變,並確認新 Primary 可以接受寫入、舊 Primary 能安全回到 Replica 角色。